Architettura x86
Riportiamo qui una vista semplificata e riassuntiva dell'architettura x86 per la quale scriveremo programmi assembler.
L'architettura x86 è a 32 bit. Questo implica che i registri generali, così come tutti gli indirizzi per locazioni in memoria, sono a 32 bit. L'evoluzione di questa architettura, x64 a 64 bit, che è quella che troviamo nei processori in commercio, è del tutto retrocompatibile.
La visione del processore che proponiamo è molto limitata, e omette diversi importanti registri, flag e funzionalità che saranno esplorati in corsi successivi.
Questi includono, per esempio, il registro ebp, la natura dei meccanismi di protezione, il significato di SEGMENTATION FAULT, e che cosa sia un kernel.
Quanto discutiamo è tuttavia sufficiente agli scopi didattici di questo corso.
Registri
I registri che utilizzeremo direttamente sono 6: eax, ebx, ecx, edx, esi, edi.
Per i primi quattro di questi, è possibile operare sulle loro porzioni a 16 e 8 bit tramite ax, ah, al e così via.
Per i registri esi ed edi è possibile operare solo sulle porzioni a 16 bit, tramite si e di.
Tipicamente, i registri eax...edx sono utilizzati per processare dati, mentre esi ed edi sono utilizzati come registri puntatori.
Questa divisione di utilizzo non è però affatto obbligatoria per la maggior parte delle istruzioni.
Altri registri sono invece utilizzati in modo indiretto:
espè il registro puntatore per la cima dello stack, viene utilizzato dapop/pushper prelevare/spostare valori nella pila, e dacall/retper la chiamata di sottoprogrammi;eipè il registro puntatore verso la prossima istruzione da eseguire, viene incrementato alla fine del fetch di una istruzione e modificato da istruzioni che cambiano il flusso d'esecuzione, comecall,rete le variejmp;eflagsè il registro dei flag, una serie di booleani con informazioni sullo stato dell'esecuzione e sul risultato dell'ultima operazione aritmetica. I flag di nostro interesse sono il carry flagCF(posizione 0), lo zero flagZF(6), il sign flagSF(7), l'overflow flagOF(11). Sono tipicamente aggiornati dalle istruzioni aritmetiche, e testati indirettamente con istruzioni condizionali comejcon,setecmov.
Di seguito uno schema funzionale dei registri del processore x86.
Memoria
Lo spazio di memoria dell'architettura x86 è indirizzato su 32 bit. Ciascun indirizzo corrisponde a un byte, ma è possibile eseguire anche letture e scritture a 16 e 32 bit.
Per tali casi è importante ricordare che l'architettura x86 è little-endian, che significa little end first, un riferimento a I viaggi di Gulliver. Questo si traduce nel fatto che quando un valore di byte viene salvato in memoria a partire dall'indirizzo , il byte meno significativo del valore viene salvato in , il secondo meno significativo in , e così via fino al più significativo in .
Questo ordinamento dei bytes in memoria non inficia sulla coerenza dei dati nei registri: eseguendo movl %eax, a e movl a, %eax il contenuto di eax non cambia, e l'ordinamento dei bit rimane coerente.
I meccanismi di protezione ci precludono l'accesso alla maggior parte dello spazio di memoria. Potremmo accedere senza incorrere in errori solo
- allo stack
- allo spazio allocato nella sezione
.data - alle istruzioni nella sezione
.text
Queste sezioni tipicamente non includono gli indirizzi "bassi", cioè a partire da 0x0.
È importante anche tenere presente che
- non è possibile eseguire istruzioni dallo stack e da
.data - non è possibile scrivere nella sezione
.text
Vanno quindi opportunamente dichiarate le sezioni, e vanno evitate operazioni di jmp, call etc. verso locazioni di .data così come le mov verso locazioni di .text.
In caso di violazione di questi meccanismi, l'errore più tipico è SEGMENTATION FAULT.
Spazio di I/O
Lo spazio di I/O, sia quello fisico (monitor, speaker, tastiera, etc.) sia quello virtuale (terminale, files su disco, etc.) ci è in realtà precluso tramite meccanismi di protezione.
Tentare di eseguire istruzioni in o out porterà infatti al brusco arresto del programma.
Il nostro programma può interagire con lo spazio di I/O solo tramite il kernel del sistema operativo.
Tutta questa complessità è astratta tramite i sottoprogrammi di input/output dell'ambiente, documentati qui.
Condizioni al reset
Il reset iniziale e l'avvio del nostro programma sono concetti completamente diversi e scollegati.
Non possiamo sfruttare nessuna ipotesi sullo stato dei registri al momento dell'avvio del nostro programma, se non che il registro eip punterà a un certo punto alla prima istruzione di _main.
Il fatto che _main sia l'entry point del nostro programma, così come l'uso di ret senza alcun valore di ritorno, è una caratteristica di questo ambiente.